﻿// RawBarcodeScanner.cs
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

namespace BarcodeScannerApp
{
    /// <summary>
    /// 通过 Raw Input 接收 HID 键盘（扫描枪）输入，
    /// 并在遇到回车或超时后输出完整条码。
    /// </summary>
    public class RawBarcodeScanner : IDisposable
    {
        // RawInput 常量
        private const int RID_INPUT = 0x10000003;
        private const int RIM_TYPEKEYBOARD = 1;
        private const int WM_INPUT = 0x00FF;
        private const uint RIDEV_INPUTSINK = 0x00000100;
        private const ushort RI_KEY_BREAK = 0x0001;

        private readonly IntPtr _hwnd;
        private StringBuilder _buffer = new StringBuilder();
        private Timer _timer;
        private bool _listening = false;

        /// <summary>
        /// 当一次扫描完成时触发，返回完整条码字符串（不包含末尾回车）。
        /// </summary>
        public event Action<string> BarcodeScanned;

        public RawBarcodeScanner(IntPtr hostWindowHandle, int timeoutMs = 100)
        {
            _hwnd = hostWindowHandle;
            InitRawInput();
            _timer = new Timer { Interval = timeoutMs };
            _timer.Tick += (s, e) => FlushBuffer();
        }

        /// <summary>
        /// 开始监听扫描枪输入
        /// </summary>
        public void Start()
        {
            _buffer.Clear();
            _timer.Stop();
            _listening = true;
        }

        /// <summary>
        /// 停止监听扫描枪输入
        /// </summary>
        public void Stop()
        {
            _listening = false;
            _timer.Stop();
            _buffer.Clear();
        }

        /// <summary>
        /// 在宿主窗体的 WndProc 中调用此方法：
        /// if (scanner.ProcessMessage(ref m)) return;
        /// </summary>
        public bool ProcessMessage(ref Message m)
        {
            if (!_listening || m.Msg != WM_INPUT)
                return false;

            ProcessRawInput(m.LParam);
            return true;
        }

        private void InitRawInput()
        {
            RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1];
            rid[0].usUsagePage = 0x01;  // Generic Desktop Controls 
            rid[0].usUsage = 0x06;  // Keyboard
            rid[0].dwFlags = RIDEV_INPUTSINK;
            rid[0].hwndTarget = _hwnd;

            if (!RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(rid[0])))
                throw new InvalidOperationException("Raw Input 注册失败");
        }

        private void ProcessRawInput(IntPtr lParam)
        {
            uint size = 0;
            GetRawInputData(lParam, RID_INPUT, IntPtr.Zero, ref size, (uint)Marshal.SizeOf<RAWINPUTHEADER>());
            if (size == 0) return;

            IntPtr bufferPtr = Marshal.AllocHGlobal((int)size);
            try
            {
                if (GetRawInputData(lParam, RID_INPUT, bufferPtr, ref size, (uint)Marshal.SizeOf<RAWINPUTHEADER>()) != size)
                    return;

                RAWINPUT raw = Marshal.PtrToStructure<RAWINPUT>(bufferPtr);
                if (raw.header.dwType == RIM_TYPEKEYBOARD &&
                    (raw.keyboard.Flags & RI_KEY_BREAK) == 0)
                {
                    char ch = VkToChar(raw.keyboard.VKey, raw.keyboard.MakeCode);
                    if (ch == '\r')
                        FlushBuffer();
                    else if (ch != '\0')
                    {
                        _buffer.Append(ch);
                        _timer.Stop();
                        _timer.Start();
                    }
                }
            }
            finally
            {
                Marshal.FreeHGlobal(bufferPtr);
            }
        }

        private void FlushBuffer()
        {
            _timer.Stop();
            if (_buffer.Length > 0)
            {
                BarcodeScanned?.Invoke(_buffer.ToString());
                _buffer.Clear();
            }
        }

        private char VkToChar(ushort vk, ushort scanCode)
        {
            byte[] ks = new byte[256];
            if (!GetKeyboardState(ks)) return '\0';

            var sb = new StringBuilder(2);
            IntPtr layout = GetKeyboardLayout(0);
            int cnt = ToUnicodeEx(vk, scanCode, ks, sb, sb.Capacity, 0, layout);
            return cnt > 0 ? sb[0] : '\0';
        }

        public void Dispose()
        {
            Stop();
            _timer?.Dispose();
        }

        #region PInvoke

        [StructLayout(LayoutKind.Sequential)]
        struct RAWINPUTDEVICE
        {
            public ushort usUsagePage;
            public ushort usUsage;
            public uint dwFlags;
            public IntPtr hwndTarget;
        }

        [StructLayout(LayoutKind.Sequential)]
        struct RAWINPUTHEADER
        {
            public uint dwType;
            public uint dwSize;
            public IntPtr hDevice;
            public IntPtr wParam;
        }

        [StructLayout(LayoutKind.Sequential)]
        struct RAWINPUT
        {
            public RAWINPUTHEADER header;
            public RAWKEYBOARD keyboard;
        }

        [StructLayout(LayoutKind.Sequential)]
        struct RAWKEYBOARD
        {
            public ushort MakeCode;
            public ushort Flags;
            public ushort Reserved;
            public ushort VKey;
            public uint Message;
            public uint ExtraInformation;
        }

        [DllImport("user32.dll", SetLastError = true)]
        static extern bool RegisterRawInputDevices(
            [In] RAWINPUTDEVICE[] pRawInputDevices,
            uint uiNumDevices,
            uint cbSize);

        [DllImport("user32.dll")]
        static extern uint GetRawInputData(
            IntPtr hRawInput,
            uint uiCommand,
            IntPtr pData,
            ref uint pcbSize,
            uint cbSizeHeader);

        [DllImport("user32.dll")]
        static extern bool GetKeyboardState(byte[] lpKeyState);

        [DllImport("user32.dll")]
        static extern IntPtr GetKeyboardLayout(uint idThread);

        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        static extern int ToUnicodeEx(
            uint wVirtKey,
            uint wScanCode,
            byte[] lpKeyState,
            [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pwszBuff,
            int cchBuff,
            uint wFlags,
            IntPtr dwhkl);

        #endregion
    }
}
